/*
 * Copyright 2019, Data61, CSIRO (ABN 41 687 119 230)
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <assert.h>
#include <stdio.h>
#include <stdbool.h>
#include <errno.h>
#include <camkes/io.h>
#include <camkes/irq.h>
#include <platsupport/io.h>
#include <platsupport/irq.h>
#include <utils/util.h>

#include <platsupport/gpio.h>
#include <gpiomuxserver_plat.h>

#include "gpio.h"

/* State management for the pins */
typedef struct gpio_entry {
    bool initialised;
    seL4_Word owner;
    gpio_t gpio;
} gpio_entry_t;

/*
 * Gotchas:
 *  - we assume that all boards that we support has an mux controller, if some
 *  pins are turned off as a result of conflicting pins via the mux, we don't
 *  actually alert the user about this, it's up to the user to not shoot
 *  themselves in the foot
 *  - on implementations that have a working 'set_next' function, we will
 *  write/read partially if there are any errors in the middle of the operation
 *  (e.g. severed link). i.e. this operation is not transactional
 *  - the pins are first come first served, and there is no way to disable the pins
 */

/* GPIO control structure for initialising pins */
static gpio_sys_t gpio_sys;
/* table for keeping track of ownership of GPIO pins */
static size_t gpio_table_size;
static gpio_entry_t *gpio_table;

/* Prototypes for these functions are not generated by the camkes templates yet */
seL4_Word the_gpio_get_sender_id();

static inline bool check_valid_gpio_id(gpio_id_t pin_id)
{
    return (pin_id < 0 || pin_id >= gpio_table_size) ? false : true;
}

static inline bool check_pin_initialised(gpio_id_t pin_id)
{
    return gpio_table[pin_id].initialised ? true : false;
}

static inline bool check_client_owns_pin(gpio_id_t pin_id, seL4_Word client_id)
{
    return gpio_table[pin_id].owner == client_id;
}

static inline seL4_Word get_client_id(void)
{
    return the_gpio_get_sender_id();
}

int the_gpio_init_pin(gpio_id_t pin_id, gpio_dir_t dir)
{

    int error = 0;

    if (!check_valid_gpio_id(pin_id)) {
        error = -EINVAL;
        goto out;
    }

    if (GPIO_DIR_IRQ_LOW <= dir && dir <= GPIO_DIR_IRQ_EDGE) {
        ZF_LOGE("Setting GPIO pins as interrupt sources is not currently supported");
        error = -EINVAL;
        goto out;
    }

    /* check if the caller owns the pin */
    seL4_Word client_id = get_client_id();
    if (check_client_owns_pin(pin_id, client_id)) {
        error = 0;
        goto out;
    }

    /* check if anyone has reserved the pin */
    if (the_gpio_get_pin_assignee(client_id) != 0) {
        error = -EBUSY;
        goto out;
    }

    /* check if anyone else has the pin */
    if (check_pin_initialised(pin_id)) {
        error = -EBUSY;
        goto out;
    }

    gpio_entry_t *gpio_entry = &gpio_table[pin_id];

    error = gpio_new(&gpio_sys, pin_id, dir, &gpio_entry->gpio);
    if (error) {
        goto out;
    }

    gpio_entry->initialised = true;
    gpio_entry->owner = client_id;

out:
    return error;
}

int the_gpio_set_level(gpio_id_t pin_id, gpio_level_t level)
{

    seL4_Word client_id = get_client_id();
    int error = 0;

    if (!check_valid_gpio_id(pin_id)) {
        error = -EINVAL;
        goto out;
    }

    if (!check_client_owns_pin(pin_id, client_id)) {
        error = -EINVAL;
        goto out;
    }

    if (level == GPIO_LEVEL_HIGH) {
        error = gpio_set(&gpio_table[pin_id].gpio);
    } else if (level == GPIO_LEVEL_LOW) {
        error = gpio_clr(&gpio_table[pin_id].gpio);
    } else {
        /* level < 0 is not valid */
        error = -EINVAL;
    }

out:
    return error;
}

int the_gpio_read_level(gpio_id_t pin_id)
{

    seL4_Word client_id = get_client_id();
    int ret = 0;

    if (!check_valid_gpio_id(pin_id)) {
        ret = -EINVAL;
        goto out;
    }

    if (!check_client_owns_pin(pin_id, client_id)) {
        ret = -EINVAL;
        goto out;
    }

    ret = gpio_get(&gpio_table[pin_id].gpio);

out:
    return ret;
}

int gpio_component_init(ps_io_ops_t *io_ops)
{
    int error = gpio_sys_init(io_ops, &gpio_sys);
    if (error) {
        ZF_LOGE("Failed to initialise GPIO subsystem");
        return error;
    }

    gpio_table_size = MAX_GPIO_ID + 1;
    error = ps_calloc(&io_ops->malloc_ops, 1, sizeof(*gpio_table) * gpio_table_size, (void **) &gpio_table);
    if (error) {
        ZF_LOGE("Failed to allocate memory for the table of GPIO pins");
        return error;
    }

    for (int i = 0; i < gpio_table_size; i++) {
        gpio_table[i].owner = -1;
    }

    return 0;
}
